﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using Tesla.Content;
using Tesla.Graphics;
using Tesla.Math;
using Tesla.Util;

namespace Tesla.Scene {
    /// <summary>
    /// Collection for managing a list of lights in the 3D world. Lights can be sorted
    /// based on an IRenderable's position in the world, otherwise a light collection is little more
    /// than a container.
    /// </summary>
    public class LightCollection : IEnumerable<Light>, ILightCollection {
        private LightComparer _comparer;
        private Color _globalAmbient;
        private Light[] _lights;
        private Light[] _tempLights;
        private int _lightCount;
        private int _maxLights;
        private bool _isEnabled;
        private bool _refreshShader;

        internal Spatial _spatial;

        /// <summary>
        /// Get or set the maximum lights to be used. If more lights
        /// are added, only the closest ones will be active. Default is 4,
        /// corresponding to the default shader library.
        /// </summary>
        public int MaxLights {
            get {
                return _maxLights;
            }
            set {
                _maxLights = value;
                _refreshShader = true;
            }
        }

        /// <summary>
        /// Gets the light at the specified index.
        /// </summary>
        /// <param name="index">Index of light in the collection</param>
        /// <returns>The light</returns>
        public Light this[int index] {
            get {
                if(index >= _lightCount || index < 0) {
                    throw new ArgumentOutOfRangeException("Index out of range.");
                }
                return _lights[index];
            }
        }

        /// <summary>
        /// Get the total number of lights. 
        /// </summary>
        public int LightCount {
            get {
                return _lightCount;
            }
        }

        /// <summary>
        /// Get or set the global ambient value. Default
        /// is black.
        /// </summary>
        public Color GlobalAmbient {
            get {
                return _globalAmbient;
            }
            set {
                _globalAmbient = value;
                _refreshShader = true;
            }
        }

        /// <summary>
        /// Get or set if this light state should be enabled.
        /// </summary>
        public bool IsEnabled {
            get {
                return _isEnabled;
            }
            set {
                _isEnabled = value;
                _refreshShader = true;
            }
        }

        /// <summary>
        /// Get or set if this light state values have
        /// changed and need to be sent to the shader.
        /// </summary>
        public bool RefreshShader {
            get {
                return _refreshShader;
            }
            set {
                _refreshShader = true;
            }
        }

        /// <summary>
        /// Creates a default light state with black global ambient
        /// color and a maximum of 4 lights.
        /// </summary>
        public LightCollection() {
            _maxLights = 4;
            _lightCount = 0;
            _lights = new Light[4];
            _isEnabled = true;
            _refreshShader = true;
            _globalAmbient = new Color(0.0f, 0.0f, 0.0f);
            _comparer = new LightComparer();
        }

        /// <summary>
        /// Creates a default light state with black global ambient
        /// color and a maximum of 4 lights. Used internally as
        /// a reference to the spatial that created it is passed to ensure
        /// if the collection changes, the spatial is updated.
        /// </summary>
        /// <param name="spatial"></param>
        internal LightCollection(Spatial spatial) {
            _maxLights = 4;
            _lightCount = 0;
            _lights = new Light[4];
            _isEnabled = true;
            _refreshShader = true;
            _globalAmbient = new Color(0.0f, 0.0f, 0.0f);
            _comparer = new LightComparer();
            _spatial = spatial;
        }

        /// <summary>
        /// Creates a light state with the specified parameters.
        /// </summary>
        /// <param name="maxLights">Maximum active lights</param>
        /// <param name="globalAmbient">Global ambient color</param>
        public LightCollection(int maxLights, Color globalAmbient) {
            _maxLights = maxLights;
            _lights = new Light[maxLights];
            _isEnabled = true;
            _refreshShader = true;
            _globalAmbient = globalAmbient;
            _comparer = new LightComparer();
        }

        private void CheckResize() {
            if(_lightCount == _lights.Length) {
                Light[] temp = new Light[_lightCount * 2];
                Array.Copy(_lights, temp, _lightCount);
                Array.Clear(_lights, 0, _lightCount);
                _lights = temp;
            }
        }

        /// <summary>
        /// Resets the collection's data and copies the input light collection's data.
        /// </summary>
        /// <param name="lights">Light collection to copy from</param>
        public void Set(ILightCollection lights) {
            Set(lights, true);
        }

        /// <summary>
        /// Sets the specified lights.
        /// </summary>
        /// <param name="lights">The lights to copy.</param>
        /// <param name="copyLights">if set to <c>true</c> [copy lights].</param>
        public void Set(ILightCollection lights, bool copyLights) {
            _maxLights = lights.MaxLights;
            _globalAmbient = lights.GlobalAmbient;
            _isEnabled = lights.IsEnabled;
            _refreshShader = true;

            if(copyLights) {
                //Clear the array
                Array.Clear(_lights, 0, _lightCount);

                //If the collection to copy from has more lights than we can carry, expand array to fit its light count
                if(lights.LightCount > _lights.Length) {
                    _lights = new Light[lights.LightCount * 2];
                }

                //Copy lights
                for(int i = 0; i < lights.LightCount; i++) {
                    _lights[i] = lights[i];
                }
                _lightCount = lights.LightCount;

                //New lights, update children
                if(_spatial != null) {
                    _spatial.PropagateDirtyDown(DirtyMark.Lighting);
                }
            }
        }

        /// <summary>
        /// Copies all the lights contained from the specified collection,
        /// but preserves the lights already present in this collection.
        /// </summary>
        /// <param name="copyFrom">Light collection to copy from</param>
        public void CopyLights(ILightCollection copyFrom) {
            int copyCount = copyFrom.LightCount;

            //Check if we need to resize
            if(copyCount + _lightCount > _lights.Length) {
                Light[] temp = new Light[(copyCount + _lightCount) * 2];
                Array.Copy(_lights, temp, _lightCount);
                Array.Clear(_lights, 0, _lightCount);
                _lights = temp;
            }

            for(int i = _lightCount, j = 0; j < copyCount; i++, j++) {
                _lights[i] = copyFrom[j];
            }
            _lightCount += copyCount;

            _refreshShader = true;
            if(_spatial != null) {
                _spatial.PropagateDirtyDown(DirtyMark.Lighting);
            }
        }

        /// <summary>
        /// Copies the specified amount of lights contained from the specified collection,
        /// but preserves the lights already present in this collection.
        /// </summary>
        /// <param name="copyFrom">Light collection to copy from</param>
        /// <param name="copyCount">Number of lights to copy</param>
        public void CopyLights(ILightCollection copyFrom, int copyCount) {
            if(copyCount > copyFrom.LightCount) {
                throw new ArgumentOutOfRangeException("Cannot copy more lights than contained in the light collection to copy from.");
            }

            if(copyCount > copyFrom.MaxLights) {
                copyCount = copyFrom.LightCount;
            }

            //Check if we need to resize
            if(copyCount + _lightCount > _lights.Length) {
                Light[] temp = new Light[(copyCount + _lightCount) * 2];
                Array.Copy(_lights, temp, _lightCount);
                Array.Clear(_lights, 0, _lightCount);
                _lights = temp;
            }

            for(int i = _lightCount, j = 0; j < copyCount; i++, j++) {
                _lights[i] = copyFrom[j];
            }

            _lightCount += copyCount;

            _refreshShader = true;
            if(_spatial != null) {
                _spatial.PropagateDirtyDown(DirtyMark.Lighting);
            }
        }

        /// <summary>
        /// Add a light to the state's list.
        /// </summary>
        /// <param name="light">Light to add</param>
        /// <returns>Index of the light or -1 if light was null</returns>
        public int Add(Light light) {
            if(light != null) {
                CheckResize();
                _lights[_lightCount++] = light;
                _refreshShader = true;
                if(_spatial != null) {
                    _spatial.PropagateDirtyDown(DirtyMark.Lighting);
                }
                return _lightCount - 1;
            }
            return -1;
        }

        /// <summary>
        /// Removes a light from the state's list.
        /// </summary>
        /// <param name="light">Light to remove</param>
        /// <returns>True if operation is successful</returns>
        public bool Remove(Light light) {
            int index = 0;
            bool found = false;
            for(int i = 0; i < _lightCount; i++) {
                if(_lights[index] == light) {
                    found = true;
                    break;
                }
                index++;
            }

            if(!found) {
                return false;
            }

            //Shift down by one
            Array.Copy(_lights, index + 1, _lights, index, _lightCount - 1);

            _lightCount--;

            _refreshShader = true;
            if(_spatial != null) {
                _spatial.PropagateDirtyDown(DirtyMark.Lighting);
            }
            return found;
        }

        /// <summary>
        /// Removes the light at the specified index.
        /// </summary>
        /// <param name="index">Zero-based index</param>
        /// <returns>True if a light was removed</returns>
        public bool RemoveAt(int index) {
            if(index >= _lightCount || index < 0) {
                throw new IndexOutOfRangeException("Index was out of range");
            }

            if(_lights[index] != null) {
                //Shift down by one
                Array.Copy(_lights, index + 1, _lights, index, _lightCount - 1);

                _lightCount--;

                _refreshShader = true;
                if(_spatial != null) {
                    _spatial.PropagateDirtyDown(DirtyMark.Lighting);
                }
                return true;
            }
            return false;
        }

        /// <summary>
        /// Clears lights from the state's list.
        /// </summary>
        public void Clear() {
            if(_lightCount == 0) {
                return;
            }

            Array.Clear(_lights, 0, _lightCount);
            if(_tempLights != null) {
                Array.Clear(_tempLights, 0, _tempLights.Length);
            }
            _lightCount = 0;

            _refreshShader = true;
            if(_spatial != null) {
                _spatial.PropagateDirtyDown(DirtyMark.Lighting);
            }
        }

        /// <summary>
        /// Sorts lights "closest" to the specified geometry that will affect it the most, only if the number of lights
        /// contained in the state exceeds the set maximum light count. 
        /// </summary>
        /// <param name="renderable">Renderable object that the lights can be sorted against based on distance</param>
        public void SortLights(IRenderable renderable) {
            //If we have less than the max lights, then no reason to sort
            if(_lightCount > _maxLights) {

                Spatial spatial = renderable as Spatial;
                if(spatial == null) {
                    return;
                }
                _comparer.SetSpatial(spatial);

                if(_tempLights == null || _tempLights.Length < _lights.Length) {
                    _tempLights = (Light[]) _lights.Clone();
                } else {
                    Array.Copy(_lights, _tempLights, _lightCount);
                }

                SortHelper.MergeSort<Light>(_tempLights, _lights, 0, _lightCount, 0, _comparer);
            }
            //However if we're sorting lights, most likely a light has changed, so refresh the shader either way.
            _refreshShader = true;
        }

        /// <summary>
        /// Serializes the object and writes it to the output.
        /// </summary>
        /// <param name="output">Savable Output</param>
        public void Write(ISavableWriter output) {
            output.Write("GlobalAmbient", _globalAmbient);
            output.Write("MaxLights", _maxLights);
            output.Write("IsEnabled", _isEnabled);
            Light[] lights = new Light[_lightCount];
            Array.Copy(_lights, lights, _lightCount);
            output.WriteSavable<Light>("Lights", lights);
        }

        /// <summary>
        /// Deserializes the object and populates it from the input.
        /// </summary>
        /// <param name="input">Savable input</param>
        public void Read(Content.ISavableReader input) {
            _globalAmbient = input.ReadColor();
            _maxLights = input.ReadInt();
            _isEnabled = input.ReadBoolean();
            _lights = input.ReadSavableArray<Light>();
        }

        #region IEnumerable<Light> Members

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<Light> GetEnumerator() {
            return (IEnumerator<Light>) _lights.GetEnumerator();
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator() {
            return _lights.GetEnumerator();
        }

        #endregion
    }
}
